package com.xixi.canal.client.starter.common;

import com.ibatis.common.xml.NodeletException;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser;
import com.ibatis.sqlmap.engine.builder.xml.SqlMapParser;
import com.ibatis.sqlmap.engine.builder.xml.XmlParserState;
import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
import com.ibatis.sqlmap.engine.transaction.TransactionConfig;
import com.ibatis.sqlmap.engine.transaction.TransactionManager;
import com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.util.ObjectUtils;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Properties;

public class SqlMapClientFactoryBean implements FactoryBean<SqlMapClient>, InitializingBean {

    private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<LobHandler>();

    /**
     * Return the LobHandler for the currently configured iBATIS SqlMapClient,
     * to be used by TypeHandler implementations like ClobStringTypeHandler.
     * <p>This instance will be set before initialization of the corresponding
     * SqlMapClient, and reset immediately afterwards. It is thus only available
     * during configuration.
     * @see #setLobHandler
     */
    public static LobHandler getConfigTimeLobHandler() {
        return configTimeLobHandlerHolder.get();
    }


    private Resource[] configLocations;

    private Resource[] mappingLocations;

    private Properties sqlMapClientProperties;

    private DataSource dataSource;

    private boolean useTransactionAwareDataSource = true;

    private Class transactionConfigClass = ExternalTransactionConfig.class;

    private Properties transactionConfigProperties;

    private LobHandler lobHandler;

    private SqlMapClient sqlMapClient;


    public SqlMapClientFactoryBean() {
        this.transactionConfigProperties = new Properties();
        this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");
    }

    /**
     * Set the location of the iBATIS SqlMapClient config file.
     * A typical value is "WEB-INF/sql-map-config.xml".
     * @see #setConfigLocations
     */
    public void setConfigLocation(Resource configLocation) {
        this.configLocations = (configLocation != null ? new Resource[] {configLocation} : null);
    }

    /**
     * Set multiple locations of iBATIS SqlMapClient config files that
     * are going to be merged into one unified configuration at runtime.
     */
    public void setConfigLocations(Resource[] configLocations) {
        this.configLocations = configLocations;
    }

    /**
     * Set locations of iBATIS sql-map mapping files that are going to be
     * merged into the SqlMapClient configuration at runtime.
     * <p>This is an alternative to specifying "&lt;sqlMap&gt;" entries
     * in a sql-map-client config file. This property being based on Spring's
     * resource abstraction also allows for specifying resource patterns here:
     * e.g. "/myApp/*-map.xml".
     * <p>Note that this feature requires iBATIS 2.3.2; it will not work
     * with any previous iBATIS version.
     */
    public void setMappingLocations(Resource[] mappingLocations) {
        this.mappingLocations = mappingLocations;
    }

    /**
     * Set optional properties to be passed into the SqlMapClientBuilder, as
     * alternative to a {@code &lt;properties&gt;} tag in the sql-map-config.xml
     * file. Will be used to resolve placeholders in the config file.
     * @see #setConfigLocation
     * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, java.util.Properties)
     */
    public void setSqlMapClientProperties(Properties sqlMapClientProperties) {
        this.sqlMapClientProperties = sqlMapClientProperties;
    }

    /**
     * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to the
     * SqlMapClient as part of a TransactionConfig instance.
     * <p>If specified, this will override corresponding settings in the SqlMapClient
     * properties. Usually, you will specify DataSource and transaction configuration
     * <i>either</i> here <i>or</i> in SqlMapClient properties.
     * <p>Specifying a DataSource for the SqlMapClient rather than for each individual
     * DAO allows for lazy loading, for example when using PaginatedList results.
     * <p>With a DataSource passed in here, you don't need to specify one for each DAO.
     * Passing the SqlMapClient to the DAOs is enough, as it already carries a DataSource.
     * Thus, it's recommended to specify the DataSource at this central location only.
     * <p>Thanks to Brandon Goodin from the iBATIS team for the hint on how to make
     * this work with Spring's integration strategy!
     * @see #setTransactionConfigClass
     * @see #setTransactionConfigProperties
     * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
     */
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * Set whether to use a transaction-aware DataSource for the SqlMapClient,
     * i.e. whether to automatically wrap the passed-in DataSource with Spring's
     * TransactionAwareDataSourceProxy.
     * <p>Default is "true": When the SqlMapClient performs direct database operations
     * outside of Spring's SqlMapClientTemplate (for example, lazy loading or direct
     * SqlMapClient access), it will still participate in active Spring-managed
     * transactions.
     * <p>As a further effect, using a transaction-aware DataSource will apply
     * remaining transaction timeouts to all created JDBC Statements. This means
     * that all operations performed by the SqlMapClient will automatically
     * participate in Spring-managed transaction timeouts.
     * <p>Turn this flag off to get raw DataSource handling, without Spring transaction
     * checks. Operations on Spring's SqlMapClientTemplate will still detect
     * Spring-managed transactions, but lazy loading or direct SqlMapClient access won't.
     * @see #setDataSource
     * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
     * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
     * @see com.ibatis.sqlmap.client.SqlMapClient
     */
    public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) {
        this.useTransactionAwareDataSource = useTransactionAwareDataSource;
    }

    /**
     * Set the iBATIS TransactionConfig class to use. Default is
     * {@code com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig}.
     * <p>Will only get applied when using a Spring-managed DataSource.
     * An instance of this class will get populated with the given DataSource
     * and initialized with the given properties.
     * <p>The default ExternalTransactionConfig is appropriate if there is
     * external transaction management that the SqlMapClient should participate
     * in: be it Spring transaction management, EJB CMT or plain JTA. This
     * should be the typical scenario. If there is no active transaction,
     * SqlMapClient operations will execute SQL statements non-transactionally.
     * <p>JdbcTransactionConfig or JtaTransactionConfig is only necessary
     * when using the iBATIS SqlMapTransactionManager API instead of external
     * transactions. If there is no explicit transaction, SqlMapClient operations
     * will automatically start a transaction for their own scope (in contrast
     * to the external transaction mode, see above).
     * <p><b>It is strongly recommended to use iBATIS SQL Maps with Spring
     * transaction management (or EJB CMT).</b> In this case, the default
     * ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations
     * without explicit transaction demarcation will execute non-transactionally.
     * <p>Even with Spring transaction management, it might be desirable to
     * specify JdbcTransactionConfig: This will still participate in existing
     * Spring-managed transactions, but lazy loading and operations without
     * explicit transaction demaration will execute in their own auto-started
     * transactions. However, this is usually not necessary.
     * @see #setDataSource
     * @see #setTransactionConfigProperties
     * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig
     * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
     * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
     * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig
     * @see com.ibatis.sqlmap.client.SqlMapTransactionManager
     */
    public void setTransactionConfigClass(Class transactionConfigClass) {
        if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) {
            throw new IllegalArgumentException("Invalid transactionConfigClass: does not implement " +
                    "com.ibatis.sqlmap.engine.transaction.TransactionConfig");
        }
        this.transactionConfigClass = transactionConfigClass;
    }

    /**
     * Set properties to be passed to the TransactionConfig instance used
     * by this SqlMapClient. Supported properties depend on the concrete
     * TransactionConfig implementation used:
     * <p><ul>
     * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit"
     * (default: false) and "SetAutoCommitAllowed" (default: true).
     * Note that Spring uses SetAutoCommitAllowed = false as default,
     * in contrast to the iBATIS default, to always keep the original
     * autoCommit value as provided by the connection pool.
     * <li><b>JdbcTransactionConfig</b> does not supported any properties.
     * <li><b>JtaTransactionConfig</b> supports "UserTransaction"
     * (no default), specifying the JNDI location of the JTA UserTransaction
     * (usually "java:comp/UserTransaction").
     * </ul>
     * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize
     * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
     * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
     * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig
     */
    public void setTransactionConfigProperties(Properties transactionConfigProperties) {
        this.transactionConfigProperties = transactionConfigProperties;
    }

    /**
     * Set the LobHandler to be used by the SqlMapClient.
     * Will be exposed at config time for TypeHandler implementations.
     * @see #getConfigTimeLobHandler
     * @see com.ibatis.sqlmap.engine.type.TypeHandler
     */
    public void setLobHandler(LobHandler lobHandler) {
        this.lobHandler = lobHandler;
    }


    public void afterPropertiesSet() throws Exception {
        if (this.lobHandler != null) {
            // Make given LobHandler available for SqlMapClient configuration.
            // Do early because because mapping resource might refer to custom types.
            configTimeLobHandlerHolder.set(this.lobHandler);
        }

        try {
            this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties);

            // Tell the SqlMapClient to use the given DataSource, if any.
            if (this.dataSource != null) {
                TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass.newInstance();
                DataSource dataSourceToUse = this.dataSource;
                if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {
                    dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);
                }
                transactionConfig.setDataSource(dataSourceToUse);
                transactionConfig.initialize(this.transactionConfigProperties);
                applyTransactionConfig(this.sqlMapClient, transactionConfig);
            }
        }

        finally {
            if (this.lobHandler != null) {
                // Reset LobHandler holder.
                configTimeLobHandlerHolder.remove();
            }
        }
    }

    /**
     * Build a SqlMapClient instance based on the given standard configuration.
     * API to build a SqlMapClient instance based on an InputStream (if possible,
     * on iBATIS 2.3 and higher) or on a Reader (on iBATIS up to version 2.2).
     * @param configLocations the config files to load from
     * @param properties the SqlMapClient properties (if any)
     * @return the SqlMapClient instance (never {@code null})
     * @throws IOException if loading the config file failed
     * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient
     */
    protected SqlMapClient buildSqlMapClient(
            Resource[] configLocations, Resource[] mappingLocations, Properties properties)
            throws IOException {

        if (ObjectUtils.isEmpty(configLocations)) {
            throw new IllegalArgumentException("At least 1 'configLocation' entry is required");
        }

        SqlMapClient client = null;
        SqlMapConfigParser configParser = new SqlMapConfigParser();
        for (Resource configLocation : configLocations) {
            InputStream is = configLocation.getInputStream();
            try {
                client = configParser.parse(is, properties);
            }
            catch (RuntimeException ex) {
                throw new NestedIOException("Failed to parse config resource: " + configLocation, ex.getCause());
            }
        }

        if (mappingLocations != null) {
            SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser);
            for (Resource mappingLocation : mappingLocations) {
                try {
                    mapParser.parse(mappingLocation.getInputStream());
                }
                catch (NodeletException ex) {
                    throw new NestedIOException("Failed to parse mapping resource: " + mappingLocation, ex);
                }
            }
        }

        return client;
    }

    /**
     * Apply the given iBATIS TransactionConfig to the SqlMapClient.
     * <p>The default implementation casts to ExtendedSqlMapClient, retrieves the maximum
     * number of concurrent transactions from the SqlMapExecutorDelegate, and sets
     * an iBATIS TransactionManager with the given TransactionConfig.
     * @param sqlMapClient the SqlMapClient to apply the TransactionConfig to
     * @param transactionConfig the iBATIS TransactionConfig to apply
     * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient
     * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions
     * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager
     */
    protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
        if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
            throw new IllegalArgumentException(
                    "Cannot set TransactionConfig with DataSource for SqlMapClient if not of type " +
                            "ExtendedSqlMapClient: " + sqlMapClient);
        }
        ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient;
        transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate().getMaxTransactions());
        extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
    }


    public SqlMapClient getObject() {
        return this.sqlMapClient;
    }

    public Class<? extends SqlMapClient> getObjectType() {
        return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class);
    }

    public boolean isSingleton() {
        return true;
    }


    /**
     * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState class).
     */
    private static class SqlMapParserFactory {

        public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) {
            // Ideally: XmlParserState state = configParser.getState();
            // Should raise an enhancement request with iBATIS...
            XmlParserState state = null;
            try {
                Field stateField = SqlMapConfigParser.class.getDeclaredField("state");
                stateField.setAccessible(true);
                state = (XmlParserState) stateField.get(configParser);
            }
            catch (Exception ex) {
                throw new IllegalStateException("iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - " +
                        "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. " + ex);
            }
            return new SqlMapParser(state);
        }
    }

}
